///
var teapo;
(function (teapo) {
// types are registered by adding variables/properties to this module
(function (EditorType) {
/**
* Resolve to a type that accepts this file.
*/
function getType(fullPath) {
// must iterate in reverse, so more generic types get used last
var reverse = Object.keys(EditorType);
for (var i = reverse.length - 1; i >= 0; i--) {
var t = this[reverse[i]];
if (t.canEdit && t.canEdit(fullPath))
return t;
}
return null;
}
EditorType.getType = getType;
})(teapo.EditorType || (teapo.EditorType = {}));
var EditorType = teapo.EditorType;
})(teapo || (teapo = {}));
///
///
///
///
///
var teapo;
(function (teapo) {
/**
* Hadles high-level application behavior,
* creates and holds DocumentStorage and FileList,
* that in turn manage persistence and file list/tree.
*
* Note that ApplicationShell serves as a top-level
* ViewModel used in Knockout.js bindings.
*/
var ApplicationShell = (function () {
function ApplicationShell(_storage) {
var _this = this;
this._storage = _storage;
this.saveDelay = 1500;
this.fileList = null;
this._selectedDocState = null;
this._editorElement = null;
this._editorHost = null;
this._saveTimeout = 0;
this._saveSelectedFileClosure = function () {
return _this._invokeSaveSelectedFile();
};
this.fileList = new teapo.FileList(this._storage);
this.fileList.selectedFile.subscribe(function (fileEntry) {
return _this._fileSelected(fileEntry);
});
// loading editors for all the files
var allFiles = this._storage.documentNames();
for (var i = 0; i < allFiles.length; i++) {
var docState = this._storage.getDocument(allFiles[i]);
docState.editor();
}
}
/**
* Prompts user for a name, creates a new file and opens it in the editor.
* Exposed as a button bound using Knockout.
*/
ApplicationShell.prototype.newFileClick = function () {
var fileName = prompt('New file');
if (!fileName)
return;
var fileEntry = this.fileList.createFileEntry(fileName);
this._storage.createDocument(fileEntry.fullPath());
fileEntry.handleClick();
};
/**
* Pops a confirmation dialog up, then deletes the currently selected file.
* Exposed as a button bound using Knockout.
*/
ApplicationShell.prototype.deleteSelectedFile = function () {
var selectedFileEntry = this.fileList.selectedFile();
if (!selectedFileEntry)
return;
if (!confirm('Are you sure deleting ' + selectedFileEntry.name()))
return;
this._storage.removeDocument(selectedFileEntry.fullPath());
this.fileList.removeFileEntry(selectedFileEntry.fullPath());
if (this._editorHost) {
this._editorHost.innerHTML = '';
}
};
/**
* Suggested name for file save operation.
*/
ApplicationShell.prototype.saveFileName = function () {
var urlParts = window.location.pathname.split('/');
return decodeURI(urlParts[urlParts.length - 1]);
};
/**
* Triggers a download of the whole current HTML, which contains the filesystem state and all the necessary code.
* Relies on blob URLs, doesn't work in old browsers.
* Exposed as a button bound using Knockout.
*/
ApplicationShell.prototype.saveHtml = function () {
var filename = this.saveFileName();
var blob = new Blob([document.documentElement.outerHTML], { type: 'application/octet-stream' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.setAttribute('download', filename);
a.click();
};
/**
* Packs the current filesystem content in a zip, then triggers a download.
* Relies on blob URLs and Zip.js, doesn't work in old browsers.
* Exposed as a button bound using Knockout.
*/
ApplicationShell.prototype.saveZip = function () {
var _this = this;
zip.useWebWorkers = false;
var filename = this.saveFileName();
if (filename.length > '.html'.length && filename.slice(filename.length - '.html'.length).toLowerCase() === '.html')
filename = filename.slice(0, filename.length - '.html'.length);
else if (filename.length > '.htm'.length && filename.slice(filename.length - '.htm'.length).toLowerCase() === '.htm')
filename = filename.slice(0, filename.length - '.htm'.length);
filename += '.zip';
zip.createWriter(new zip.BlobWriter(), function (zipWriter) {
var files = _this._storage.documentNames();
var completedCount = 0;
for (var i = 0; i < files.length; i++) {
var docState = _this._storage.getDocument(files[i]);
var content = docState.getProperty(null);
var zipRelativePath = files[i].slice(1);
zipWriter.add(zipRelativePath, new zip.TextReader(content), function () {
completedCount++;
if (completedCount === files.length) {
zipWriter.close(function (blob) {
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.setAttribute('download', filename);
a.click();
});
}
});
}
});
};
/**
* Invoked from the Knockout/view side to pass the editor host DIV
* to ApplicationShell.
*/
ApplicationShell.prototype.attachToHost = function (editorHost) {
this._editorHost = editorHost;
if (this._editorElement) {
this._editorHost.innerHTML = '';
this._editorHost.appendChild(this._editorElement);
}
};
ApplicationShell.prototype._fileSelected = function (fileEntry) {
var _this = this;
var newDocState = null;
if (fileEntry)
newDocState = this._storage.getDocument(fileEntry.fullPath());
if (this._selectedDocState) {
// save file if needed before switching
if (this._saveTimeout) {
clearTimeout(this._saveTimeout);
this._selectedDocState.editor().save();
}
// close file before switching
this._selectedDocState.editor().close();
}
var newEditorElement = null;
if (newDocState) {
var onchanged = function () {
return _this._selectedFileEditorChanged();
};
newEditorElement = newDocState.editor().open(onchanged);
}
if (newEditorElement !== this._editorElement) {
var oldEditorElement = this._editorElement;
this._editorElement = newEditorElement;
if (oldEditorElement && this._editorHost) {
this._editorHost.removeChild(oldEditorElement);
}
this._editorHost.innerHTML = ''; // removing the initial startup decoration
if (newEditorElement && this._editorHost)
this._editorHost.appendChild(newEditorElement);
}
};
ApplicationShell.prototype._selectedFileEditorChanged = function () {
if (this._saveTimeout)
clearTimeout(this._saveTimeout);
this._saveTimeout = setTimeout(this._saveSelectedFileClosure, this.saveDelay);
};
ApplicationShell.prototype._invokeSaveSelectedFile = function () {
var selectedFileEntry = this.fileList.selectedFile();
if (!selectedFileEntry)
return;
var docState = this._storage.getDocument(selectedFileEntry.fullPath());
docState.editor().save();
};
return ApplicationShell;
})();
teapo.ApplicationShell = ApplicationShell;
})(teapo || (teapo = {}));
///
///
///
var teapo;
(function (teapo) {
/**
* Basic implementation for a text-based editor.
*/
var CodeMirrorEditor = (function () {
function CodeMirrorEditor(_shared, docState) {
this._shared = _shared;
this.docState = docState;
this._doc = null;
this._text = null;
}
CodeMirrorEditor.standardEditorConfiguration = function () {
return {
lineNumbers: true,
matchBrackets: true,
autoCloseBrackets: true,
matchTags: true,
showTrailingSpace: true,
autoCloseTags: true,
highlightSelectionMatches: { showToken: /\w/ },
styleActiveLine: true,
tabSize: 2,
extraKeys: { "Tab": "indentMore", "Shift-Tab": "indentLess" }
};
};
/**
* Invoked when a file is selected in the file list/tree and brought open.
*/
CodeMirrorEditor.prototype.open = function (onchange) {
this._shared.editor = this;
// storing passed function
// (it should be invoked for any change to trigger saving)
this._invokeonchange = onchange;
// this may actually create CodeMirror instance
var editor = this.editor();
editor.swapDoc(this.doc());
// invoking overridable logic
this.handleOpen();
var element = this._shared.element;
if (element && !element.parentElement)
setTimeout(function () {
return editor.refresh();
}, 1);
return element;
};
/**
* Invoked when file needs to be saved.
*/
CodeMirrorEditor.prototype.save = function () {
// invoking overridable logic
this.handleSave();
};
/**
* Invoked when file is closed (normally it means another one is being opened).
*/
CodeMirrorEditor.prototype.close = function () {
if (this._shared.editor === this)
this._shared.editor = null;
// should not try triggering a save when not opened
this._invokeonchange = null;
this.handleClose();
};
CodeMirrorEditor.prototype.remove = function () {
this.handleRemove();
};
/**
* Retrieve CodeMirror.Doc that is solely used for this document editing.
*/
CodeMirrorEditor.prototype.doc = function () {
if (!this._doc)
this._initDoc();
return this._doc;
};
/**
* Retrieve CodeMirror editor that normally is shared with other documents of the same type.
* Be careful not to use it when this specific document is closed.
*/
CodeMirrorEditor.prototype.editor = function () {
// note that editor instance is shared
if (!this._shared.cm)
this._initEditor();
return this._shared.cm;
};
/**
* Retrieve the text of this document.
* This property is cached, so retrieving the text is cheap between the edits.
* If the document has never been edited, the text is retrieved from the storage instead,
* which is much cheaper still.
*/
CodeMirrorEditor.prototype.text = function () {
if (!this._text) {
if (this._doc)
this._text = this._doc.getValue();
else
this._text = this.docState.getProperty(null) || '';
}
return this._text;
};
/**
* Overridable method, invoked when the document is being opened.
*/
CodeMirrorEditor.prototype.handleOpen = function () {
};
/**
* Overridable method, invoked when the document has been changed.
* CodeMirrorEditor subscribes to corresponding event internally, and does some internal handling before invoking handleChange.
*/
CodeMirrorEditor.prototype.handleChange = function (change) {
};
/**
* Overridable method, invoked when the document is being closed.
*/
CodeMirrorEditor.prototype.handleClose = function () {
};
/**
* Overridable method, invoked when the file was removed and the editor needs to be destroyed.
*/
CodeMirrorEditor.prototype.handleRemove = function () {
};
/**
* Overridable method, invoked when the document is being loaded first time from the storage.
* The default implementation fetches 'null' property from the storage.
* Keep calling super.handleLoad() if that is the desired behavior.
*/
CodeMirrorEditor.prototype.handleLoad = function () {
if (this.docState) {
this.doc().setValue(this.docState.getProperty(null) || '');
this.doc().clearHistory();
}
};
/**
* Overridable method, invoked when the document needs to be saved.
* The default implementation stores into 'null' property of the storage.
* Keep calling super.handleSave() if that is the desired behavior.
*/
CodeMirrorEditor.prototype.handleSave = function () {
if (this.docState)
this.docState.setProperty(null, this.text());
};
CodeMirrorEditor.prototype._initEditor = function () {
var _this = this;
var options = this._shared.options || CodeMirrorEditor.standardEditorConfiguration();
this._shared.cm = new CodeMirror(function (element) {
return _this._shared.element = element;
}, options);
};
CodeMirrorEditor.prototype._initDoc = function () {
var _this = this;
// resolve options (allow override)
var options = this._shared.options || CodeMirrorEditor.standardEditorConfiguration();
this._doc = options.mode ? new CodeMirror.Doc('', options.mode) : new CodeMirror.Doc('');
// invoke overridable handleLoad()
this.handleLoad();
// subscribe to change event
CodeMirror.on(this._doc, 'change', function (instance, change) {
// it is critical that _text is cleared on any change
_this._text = null;
// notify the external logic that the document was changed
_this._invokeonchange();
_this.handleChange(change);
});
};
return CodeMirrorEditor;
})();
teapo.CodeMirrorEditor = CodeMirrorEditor;
/**
* Simple document type using CodeMirrorEditor, usable as a default type for text files.
*/
var PlainTextEditorType = (function () {
function PlainTextEditorType() {
this._shared = {};
}
PlainTextEditorType.prototype.canEdit = function (fullPath) {
return true;
};
PlainTextEditorType.prototype.editDocument = function (docState) {
return new CodeMirrorEditor(this._shared, docState);
};
return PlainTextEditorType;
})();
(function (EditorType) {
/**
* Registering PlainTextEditorType.
*/
EditorType.PlainText = new PlainTextEditorType();
})(teapo.EditorType || (teapo.EditorType = {}));
var EditorType = teapo.EditorType;
})(teapo || (teapo = {}));
///
///
///
///
///
///
function start() {
var storage = null;
var viewModel = null;
var storageLoaded = function () {
teapo.registerKnockoutBindings(ko);
viewModel = new teapo.ApplicationShell(storage);
var pageElement = null;
for (var i = 0; i < document.body.childNodes.length; i++) {
var e = document.body.childNodes.item(i);
if (e && e.tagName && e.tagName.toLowerCase() !== 'script') {
if (e.className && e.className.indexOf('teapo-page') >= 0) {
pageElement = e;
continue;
}
document.body.removeChild(e);
i--;
}
}
ko.renderTemplate('page-template', viewModel, null, pageElement);
};
teapo.openStorage({
documentStorageCreated: function (error, s) {
storage = s;
storageLoaded();
},
getType: function (fullPath) {
return teapo.EditorType.getType(fullPath);
},
getFileEntry: function (fullPath) {
return viewModel.fileList.getFileEntry(fullPath);
}
});
}
// TODO: remove this ridiculous timeout (need to insert scripts above teapo.js)
setTimeout(start, 100);
//# sourceMappingURL=teapo.js.map